Глава 4. Фундаментальные основы .Net

Инструмент для просмотра машинного кода .net и исполняемых файлов
https://github.com/dnSpyEx/dnSpy/releases/tag/v6.5.1
Очень удобно

Устройство простой dll

Каждая секция содержит в начале поля:

  1. Начальный адрес в виртуальной памяти
  2. Размер в виртуальной памяти
  3. Начальный адрес в файле
  4. Размер секции в файле
    В конце есть поле о числе строк, но оно устарело, применялось в с++ для дебага. Сейчас вся инфа о строках хранится в pdb

Общая структура файла такая:

  1. PE Заголовок, тут все по базе, заголовок только чтобы сказать что мы PE
  2. DOS Заголовок как легаси, содержит строку о том что DOS не поддерживаем
  3. Обычный заголовок с общей системной инфой
  4. Описание секции текст, секция нужна для хранения кода и метаданных
  5. Секция ресурсов, нужно например для иконки
  6. Секция reloc нужна для инфы о смещениях которые нужно совершить, если начальный адрес из заголовков PE придется смещать (оно для ОС)
  7. Заголовок Cor20 он же Clr заголовок. Содержит ссылку на начало метаданных
  8. Заголовок метаданных хранит описание 5 потоков. Включает в себя название, размер потока и отступ от начала метаданных
#~ или #- Таблицы метаданных (compressed / uncompressed формат)
#Strings Таблица строк (имена классов, методов и пр.)
#US User Strings (строковые литералы из IL)
#Blob Сигнатуры методов, массивы, параметры
#GUID GUID'ы сборки

Pasted image 20250505003124.png

Отдельно про поток #~
Pasted image 20250505011103.png
в нем хранятся таблицы метаданных
Каждая таблица — это структурированный набор записей, похожий на строки базы данных. Каждая строка описывает что-то: тип, метод, атрибут и т.д.

Таблица Назначение
00 Module Информация о текущем модуле (сборке .dll/.exe). Одна строка.
01 TypeRef Ссылки на типы, определённые в других сборках.
02 TypeDef Определения всех типов в этой сборке.
06 MethodDef Все методы, определённые в сборке.
08 Param Описания параметров методов.
0A MemberRef Ссылки на поля/методы, определённые вне текущей сборки.
0C CustomAttribute Все кастомные атрибуты (например, [Serializable]).
11 StandAloneSig Сигнатуры (например, для лямбд или delegate).
1B TypeSpec Специализации generic-типов (например, List<int>).
20 Assembly Информация о сборке (имя, версия и пр.).
23 AssemblyRef Ссылки на внешние сборки.
2B MethodSpec Generic-специализации методов (Do<T>()).

Для обращения к метаданных из IL кода применяются токены
формат токена
Токен = [таблица или поток (8 бит) ] + [индекс для таблиц/смещение для потока со строками(24 бита)]

Таблицы представлены буквально как таблицы в постгре, то есть идут записи подряд
структура каждой таблицы метаданных в потоке #~ жёстко задана стандартом ECMA-335 — то есть она не описывается в самом PE-файле

Пример структуры таблицы:

TypeDef Row Layout (обычно):

  • Flags (4 байта)
  • TypeName (index into #Strings)
  • TypeNamespace (index into #Strings)
  • Extends (coded index to TypeDefOrRef)
  • FieldList (index to Field table)
  • MethodList (index to Method table)

Домены приложений и сборки

В .Net домены приложений, AppDomains - это изначально механизм изоляции выполнения программ внутри одного процесса. Данный механизм реализовывался на уровне CLR.

AppDomain - это логически изолированная среда. Она:

  1. Изолирует сборки, сборка обычно это длл или ехе
  2. Позволяет динамически загружать и выгружать их
    Благодаря раздельным таким подпроцессам в рамках одного процесса, можно было загружать внешние модули, не опасаясь, что они повредят программу.
    До появления async Task, Домены были способом параллельного исполнения

В .net Core, механизм изоляции и выгрузки больше не поддерживается полноценно.

Осталось:

  1. В приложении существует AppDomain.CurrentDomain - текущий домен приложения, он всегда один.
  2. Доступны свойства текущего домена

Что удалено:

  1. Создание новых доменов
  2. Выгрузка доменов
  3. Изоляция кода по доменам

Применяются отдельные процессы и AssemblyLoadContext()

Теперь таким образом можно создавать свои контексты сборки и загружать туда длл в виде файла.

пример кода с загрузкой длл

var context = new AssemblyLoadContext("PluginContext", isCollectible: true);
var assembly = context.LoadFromAssemblyPath("/plugins/MyPlugin.dll");
var type = assembly.GetType("MyPluginNamespace.MyPluginClass");
var instance = Activator.CreateInstance(type);

Процесс выгрузки сборки, сработает только если isCollectible: true

var context = new AssemblyLoadContext("UnloadableContext", isCollectible: true);
Assembly assembly = context.LoadFromAssemblyPath("/path/to.dll");

// Работа с типами...

context.Unload(); // Помечает на выгрузку
GC.Collect(); GC.WaitForPendingFinalizers(); // Чтобы реально выгрузилось

⚠️ Важно: выгрузка работает только при отсутствии живых ссылок на типы из загруженной сборки.

По умолчанию .NET использует AssemblyLoadContext.Default.

Когда вы создаёте новый AssemblyLoadContext, CLR создает изолированное пространство загрузки, где типы и сборки не пересекаются с другими контекстами.

Может быть сборкой-разгружаемым (isCollectible: true) или обычным (в памяти навсегда).

Сравнение уровней изоляции доменов и контекстов

Характеристика AppDomain (.NET Framework) AssemblyLoadContext (.NET Core / .NET 5+)
Изоляция памяти Частичная — данные и стеки изолированы Только изоляция сборок (DLL), память общая
Изоляция объектов Полная: нельзя передать объекты напрямую Нет: объекты общие, если типы совпадают
Механизм безопасности (CAS херня для безопасности кода из винды) Поддерживает CAS (ограничения прав) Не поддерживается
Граница сериализации Да (можно передавать только сериализуемые) Нет: объект = объект
Сборка мусора Уничтожается вся область домена Выгружается только если isCollectible = true

Тип определяется не только по имени, но и по контексту загрузки сборки.

Итог про домены, сборки и контексты

Домен это мега изоляция почти как процессы, но на уровне CLR, но все это обрезали в новых версиях

В рамках одного процесса может быть несколько контекстов, в рамках одного контекста свои переменные и типы. Типы и переменные из двух разных контекстов взаимодействовать друг с другом не могут.
Но! В рамках одного контекста может быть несколько сборок (То есть DLL), и переменные между ними могут взаимодействовать, но нужно учитывать что описание типа - не только название но и сборка. Поэтому, например для каста между разными сборками, можно сделать сборку contracts.dll где описать общие для этих двоих интерфейсы и касты между ними.
Пример кастов между сборками в рамках одного контекста:

var context = new AssemblyLoadContext("MyContext");

var contracts = context.LoadFromAssemblyPath("Contracts.dll");
var pluginA = context.LoadFromAssemblyPath("PluginA.dll");
var pluginB = context.LoadFromAssemblyPath("PluginB.dll");

var typeA = pluginA.GetType("PluginA.MyPlugin");
var typeB = pluginB.GetType("PluginB.MyPlugin");

var objA = Activator.CreateInstance(typeA);
var objB = Activator.CreateInstance(typeB);

// Получаем Type интерфейса из загруженной Contracts.dll
var interfaceType = contracts.GetType("Contracts.IPlugin");

// ✅ Кастинг сработает, потому что это один и тот же Type
bool isAPlugin = interfaceType.IsInstanceOfType(objA); // true
bool isBPlugin = interfaceType.IsInstanceOfType(objB); // true

// Можно привести:
var plugin = (dynamic)objA;
plugin.Run(); // если метод Run есть

Существование контекстов обусловлено системами, которые могут одновременно использовать библиотеки разных версий, и чтобы изолировать типы этих библиотек, их можно подключать в разных контекстах.

Области памяти процесса

Pasted image 20250513020252.png
Commited память включает в себя ту память, которую ОС выложила на реальную, но возможно на swap возможно в рам
Private WS это память которая выложена в именно в РАМ и которая не общая

Shareable (около 2 ГиБ) – разделяемая память, которая нас не особенно интересует; Эти области служат для целей системного управления, вообще не имеющих отношения к .NET;

Mapped File (около 4 МиБ) – как отмечалось в главе 2, эти области содержат проецируемые файлы, в частности шрифты и файлы локализации. Хотя они и читаются средой выполнения .NET с применением различных API локализации, никаких проблем нашему приложению они создавать не должны;

Image (около 37 МиБ) – двоичные образы, содержащие различные исполняемые файлы .NET, включая саму среду выполнения и нашу сборку. Отметим, что большая часть этой области разделяемая, и лишь 772 КиБ входят в частный рабочий набор. Это файлы, которые читаются с диска на этапе запуска приложения;

Stack (около 4,5 МиБ) – в нашем приложении Hello World три потока, поэтому для них отведено три области под стеки;
Pasted image 20250513020544.png

Heap и Private Data (около 9 МиБ) – это различные области памяти, которые среда выполнения .NET использует для собственных целей. Среди них есть фундаментальные структуры данных например:

  • список пометки и таблицы карт, с которыми мы познакомимся в главах 5, 8 и 11;
  • здесь хранятся данные о регистрации интернированных строк;
  • Последние области памяти помечены флагами защиты Выполнение/Чтение/Запись. Сюда JIT-компилятор помещает машинный код при компиляции CIL-кода. Потому-то они и помечены флагом выполнения (Execute), поскольку этот код должен допускать вызов, как любой другой.
    Если по какой-то причине в нашем приложении часто производится JIT-компиляция, то мы будем наблюдать постоянный рост таких частных областей с флагами Выполнение/Чтение/ Запись; 
    
  • различные временные области, необходимые во время JIT-компиляции;
    Pasted image 20250513021156.png
    Managed Heap включает в себя:
    1. Куча GC – самая важная для нас куча, которой управляет сборщик мусора. Большая часть типов нашего приложения создается здесь.
    2. Куча загрузчика, содержит много областей необходимых домену для работы CLR
    1. Высокочастотная куча, содержит те данные, к которым CLR часто обращается, например здесь хранятся описания методов и полей. Здесь же находятся статические данные примитивных типов
    2. Низкочастотная куча, тут хранится то что CLR слабо редко нужно
    3. Много куч с данными для работы CLR
    По причине того, что куча загрузчика никогда не чиститься, если мы будем динамически загружать в память много-много типов, то потребление памяти будет быстро расти и никогда не падать.

Page Table (небольшая область размером 36 КиБ) – таблица страниц

Система Типов